/**
 * Copyright (C) 2013 DaiKit.com - daikit4gxt module (admin@daikit.com)
 *
 *         Project home : http://code.daikit.com/daikit4gxt
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.daikit.daikit4gxt.client.controller;

import java.util.HashMap;
import java.util.Map;

import com.daikit.commons.shared.bean.BaseUser;
import com.daikit.commons.shared.utils.DkObjectUtils;
import com.daikit.daikit4gxt.client.DkMain;
import com.daikit.daikit4gxt.client.action.BaseAction;
import com.daikit.daikit4gxt.client.action.processor.DkConnectionReturnProcessor;
import com.daikit.daikit4gxt.client.action.processor.DkStandardConnectionReturnProcessor;
import com.daikit.daikit4gxt.client.action.standard.BaseConnectAction;
import com.daikit.daikit4gxt.client.action.standard.BaseDisconnectAction;
import com.daikit.daikit4gxt.client.action.standard.BaseInvalidateUiAction;
import com.daikit.daikit4gxt.client.action.standard.BaseLoadInitializationDataAction;
import com.daikit.daikit4gxt.client.action.standard.BaseOnApplicationLoadedAction;
import com.daikit.daikit4gxt.client.action.standard.BaseOnStandaloneApplicationLoadedAction;
import com.daikit.daikit4gxt.client.action.standard.BaseReloadCurrentScreenAction;
import com.daikit.daikit4gxt.client.action.standard.BaseShowScreenAction;
import com.daikit.daikit4gxt.client.event.DkDisconnectEvent;
import com.daikit.daikit4gxt.client.event.DkDisconnectEvent.DisconnectEventHandler;
import com.daikit.daikit4gxt.client.event.DkDisconnectEvent.HasDisconnectEventHandlers;
import com.daikit.daikit4gxt.client.exception.ApplicationInitializationException;
import com.daikit.daikit4gxt.client.log.BaseLogger;
import com.daikit.daikit4gxt.client.rpc.BaseRpcMiscs;
import com.daikit.daikit4gxt.client.rpc.BaseRpcMiscsAsync;
import com.daikit.daikit4gxt.client.rpc.BaseRpcUser;
import com.daikit.daikit4gxt.client.rpc.BaseRpcUserAsync;
import com.daikit.daikit4gxt.client.screen.Screen;
import com.daikit.daikit4gxt.client.screen.WelcomeScreen;
import com.daikit.daikit4gxt.client.ui.BaseGui;
import com.daikit.daikit4gxt.client.ui.popup.ConnectionPopup;
import com.daikit.daikit4gxt.shared.bean.ConnectionData;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.sencha.gxt.widget.core.client.Component;
import com.sencha.gxt.widget.core.client.form.FormPanel;


/**
 * Application controller class that all applications should extend.
 * 
 * @author tcaselli
 * @version $Revision$ Last modifier: $Author$ Last commit: $Date$
 */
public abstract class BaseMainController implements HasDisconnectEventHandlers
{

	private final BaseLogger log = BaseLogger.getLog(BaseMainController.class);

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// ATTRIBUTES
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * The Graphic User Interface.
	 */
	private final BaseGui gui;
	private Screen currentScreen = null;
	private Screen previousScreen = null;
	private DkConnectionReturnProcessor connectionReturnProcessor = new DkStandardConnectionReturnProcessor();
	private String connectionPopupMessage = null;
	private boolean isInvalidatingUI = false;
	private String screenDisplayId;

	private final Map<String, Screen> createdScreens = new HashMap<String, Screen>();

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// CREATION
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * Only constructor.
	 * 
	 * @param gui
	 *           the application graphical user interface extending {@link BaseGui}
	 */
	public BaseMainController(final BaseGui gui)
	{
		this.gui = gui;
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// ACTIONS
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * @return whether a chain action is currently running.
	 */
	public boolean isChainActionRunning()
	{
		return BaseAction.isChainRunning();
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// CONNECTION
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * @return the connection popup instance
	 */
	public ConnectionPopup getConnectionPopupInstance()
	{
		return ConnectionPopup.instance();
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// SCREEN MANAGEMENT
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * Call onBeforeScreenLeft() on the current screen if not null.
	 * 
	 * @param currentAction
	 *           the current chain action. To be able to restart it after.
	 * @return whether or not the current screen can be changed to the given currentScreen.
	 */
	public boolean onBeforeScreenLeft(final BaseAction<?> currentAction)
	{
		return this.currentScreen == null || this.currentScreen.onBeforeScreenLeft(currentAction);
	}

	/**
	 * Call onScreenBeforeShow(previousDisplayedScreen) on the current screen. After the call to this function the
	 * mainController.currentScreen is set to this and the show screen process (layout and chain actions) is done.
	 * 
	 * @param previousDisplayedScreen
	 *           the previous displayed screen
	 * @param optionalArgs
	 *           optional arguments
	 */
	public void onScreenBeforeShow(final Screen previousDisplayedScreen, final Object... optionalArgs)
	{
		if (currentScreen != null)
		{
			currentScreen.baseBeforeScreenShow(previousDisplayedScreen, optionalArgs);
		}
	}

	/**
	 * Create a {@link Screen} instance from the given class.<br>
	 * This screen must be specified in the {@link #getSpecificScreenInstance(Class)} implementation.
	 * 
	 * @param clazz
	 *           the class of the screen to be created
	 * @return the created screen instance
	 */
	@SuppressWarnings("unchecked")
	public final <S extends Screen> S getScreenInstance(final Class<S> clazz)
	{
		S ret = (S) createdScreens.get(clazz.getName());
		if (ret == null)
		{
			final String className = clazz.getName();
			if (WelcomeScreen.class.getName().equals(className))
			{
				ret = (S) new WelcomeScreen();
				createdScreens.put(className, ret);
			}
			else
			{
				ret = (S) getSpecificScreenInstance(clazz);
				if (ret != null)
				{
					createdScreens.put(className, ret);
				}
				else
				{
					throw new ApplicationInitializationException("The given Screen class : " + className
							+ " is not referenced in the Screen Creator.");
				}
			}
			log.debug("[ShowScreenAction Screen {" + className + "} Created]");
		}
		return ret;
	}

	/**
	 * Method to be overridden to provide a way to access the default application screen. This screen will be loaded
	 * first when the application starts.
	 * 
	 * @return the default {@link Screen} class.
	 */
	public abstract Class<? extends Screen> getDefaultScreen();

	/**
	 * Method to be used to know if a screen is the currently displayed screen.
	 * 
	 * @param screenClass
	 *           the {@link Screen} class to test if it is the currently displayed screen.
	 * 
	 * @return whether the given screen is the current one
	 */
	public boolean isCurrentScreen(final Class<? extends Screen> screenClass)
	{
		return DkObjectUtils.equalsWithNull(screenClass, getCurrentScreen() == null ? null : getCurrentScreen().getClass());
	}

	/**
	 * Show the screen corresponding to the given class.<br>
	 * Given parameters will be sent to the screen via the {@link Screen#initializeScreen(Object...)} and
	 * {@link Screen#getReloadScreenAction(boolean, Object...)} methods.
	 * 
	 * @param screen
	 *           the {@link Screen} to be shown.
	 * @param initializationArgs
	 *           parameters to be sent to the screen for initialization and reload.
	 */
	public void showScreen(final Class<? extends Screen> screen, final Object... initializationArgs)
	{
		BaseShowScreenAction.get(screen, initializationArgs).execute();
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// REFRESH / RELOAD CURRENT SCREEN
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * Wrapper to {@link #refreshScreen(boolean, Object...)} with force=false and no optional argument
	 */
	public void refreshScreen()
	{
		refreshScreen(false);
	}

	/**
	 * Refresh current screen by getting the screen reload action and executing it after having called
	 * {@link Screen#onBeforeScreenRefresh(BaseAction)}
	 * 
	 * @param force
	 *           to force layout refresh
	 * @param optionalArgs
	 *           to pass optional arguments to {@link Screen#getReloadScreenAction(boolean, Object...)} method.
	 */
	public void refreshScreen(final boolean force, final Object... optionalArgs)
	{
		final BaseAction<?> reloadScreenAction = BaseReloadCurrentScreenAction.get(force, optionalArgs);
		if (reloadScreenAction != null)
		{
			if (currentScreen.onBeforeScreenRefresh(reloadScreenAction))
			{
				reloadScreenAction.execute();
			}
		}
		else
		{
			log.error("Don't call Main.controller().refreshScreen() before having shown the screen.");
		}
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// URL METHODS
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * Call URL , for example to download a file.
	 * 
	 * @param url
	 *           the url to be called (must be absolute starting with http://)
	 */
	public void callUrl(final String url)
	{
		final FormPanel hyperlinkFormPanel = gui.getHyperlinkCallerFormPanel();
		hyperlinkFormPanel.setAction(url);
		log.debug("CALL URL : " + url);
		hyperlinkFormPanel.submit();
	}

	/**
	 * Open a new window (or tab depending on the browser) and display the page with the given url.
	 * 
	 * @param url
	 *           the url to be called (must be absolute starting with http://)
	 */
	public void openUrlInNewWindow(final String url)
	{
		openHyperlinkInNewWindow(url);
	}

	private static native void openHyperlinkInNewWindow(String url)/*-{
																						window.open(url);
																						}-*/;

	/**
	 * Open a new tab (or window depending on the browser) and display the page with the given url.
	 * 
	 * @param url
	 *           the url to be called (must be absolute starting with http://)
	 */
	public void openUrlInNewTab(final String url)
	{
		openHyperlinkInNewTab(url);
	}

	private static native void openHyperlinkInNewTab(String url)/*-{
																					window.open(url, '_blank');
																					}-*/;

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// METHODS TO BE OVERRIDEN
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * Method intended to create Screen instances according to their Class. This is useful because there is no way to
	 * instantiate an Object from its class excepted with a call to "new Class(...)". Class.newInstance() is not
	 * compilable with GWT.
	 * 
	 * @param clazz
	 *           the Class for the instance to be created.
	 * @return a Screen object
	 */
	protected abstract <S extends Screen> Screen getSpecificScreenInstance(Class<S> clazz);

	/**
	 * Return the action to be executed after Connection success. May be overridden to provide custom action.
	 * 
	 * @param previousLoggedUser
	 *           the previous logged user
	 * @return the created {@link BaseAction}
	 */
	public BaseAction<?> getSpecificAfterConnectAction(final BaseUser previousLoggedUser)
	{
		return BaseLoadInitializationDataAction.get();
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// RPCS
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	protected BaseRpcUserAsync rpcUser;
	protected BaseRpcMiscsAsync rpcMiscs;

	/**
	 * Method to be overridden if you want to override {@link BaseRpcUser}
	 * 
	 * @return the asynchronous interface client side implementation.
	 */
	public BaseRpcUserAsync baseRpcUser()
	{
		if (rpcUser == null)
		{
			rpcUser = GWT.create(BaseRpcUser.class);
		}
		return rpcUser;
	}

	/**
	 * Method to be overridden if you want to override {@link BaseRpcMiscs}
	 * 
	 * @return the asynchronous interface client side implementation.
	 */
	public BaseRpcMiscsAsync baseRpcMiscs()
	{
		if (rpcMiscs == null)
		{
			rpcMiscs = GWT.create(BaseRpcMiscs.class);
		}
		return rpcMiscs;
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// METHODS - UI
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * Do invalidate UI without action (GUI+ currentScreen). This method is also called by the
	 * {@link BaseInvalidateUiAction}.
	 * 
	 * @param doForceLayout
	 *           to force the application layout refresh
	 */
	public void invalidateUi(final boolean doForceLayout)
	{
		isInvalidatingUI = true;
		gui.invalidateUi();
		if (currentScreen != null)
		{
			currentScreen.baseInvalidateUi();
		}
		if (doForceLayout)
		{
			gui.getViewport().forceLayout();
		}
		isInvalidatingUI = false;
	}

	/**
	 * Method called when user wants to changed full screen status.
	 * 
	 * @param fullScreen
	 *           indicating whether the application should now be displayed in full screen.
	 */
	public void onFullScreen(final boolean fullScreen)
	{
		DkMain.model().setFullSize(fullScreen);
		gui.onFullScreenChanged();
		if (currentScreen != null)
		{
			currentScreen.baseOnFullScreenChanged();
		}
		invalidateUi(true);
	}

	/**
	 * Method called once the application is loaded. Wrapper to {@link #getOnApplicationLoadedAction()}.execute()
	 */
	public void onApplicationLoaded()
	{
		getOnApplicationLoadedAction().execute();
	}

	/**
	 * 
	 * @return the {@link BaseAction} to be executed once the application is loaded.
	 */
	public BaseAction<?> getOnApplicationLoadedAction()
	{
		return DkMain.config().isStandalone() ? BaseOnStandaloneApplicationLoadedAction.get() : BaseOnApplicationLoadedAction.get();
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// GETTERS / SETTERS
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * @return the graphical user interface extending {@link BaseGui}
	 */
	public BaseGui getGui()
	{
		return gui;
	}

	/**
	 * @return the current screen extending {@link Screen}
	 */
	public Screen getCurrentScreen()
	{
		return currentScreen;
	}

	/**
	 * Set the current screen. This method <b>MUST NOT</b> be called directly but only via the action
	 * {@link BaseShowScreenAction} or the controller method {@link #showScreen(Class, Object...)}
	 * 
	 * @param currentScreen
	 *           the screen instance to be shown.
	 */
	public final void setCurrentScreen(final Screen currentScreen)
	{
		this.currentScreen = currentScreen;
	}

	/**
	 * @return the previous screen extending {@link Screen}
	 */
	public Screen getPreviousScreen()
	{
		return previousScreen;
	}

	/**
	 * Sets the previous displayed screen. This method <b>MUST NOT</b> be called directly but only via the action
	 * {@link BaseShowScreenAction} or the controller method {@link #showScreen(Class, Object...)}
	 * 
	 * @param previousScreen
	 *           the previousScreen to set
	 */
	public void setPreviousScreen(final Screen previousScreen)
	{
		this.previousScreen = previousScreen;
	}

	/**
	 * @return the connectionPopupMessage
	 */
	public String getConnectionPopupMessage()
	{
		return connectionPopupMessage;
	}

	/**
	 * @param connectionPopupMessage
	 *           the connectionPopupMessage to set
	 */
	public void setConnectionPopupMessage(final String connectionPopupMessage)
	{
		this.connectionPopupMessage = connectionPopupMessage;
	}

	/**
	 * @return the connectionReturnProcessor
	 */
	public DkConnectionReturnProcessor getConnectionReturnProcessor()
	{
		return connectionReturnProcessor;
	}

	/**
	 * @param connectionReturnProcessor
	 *           the connectionReturnProcessor to set
	 */
	public void setConnectionReturnProcessor(final DkConnectionReturnProcessor connectionReturnProcessor)
	{
		this.connectionReturnProcessor = connectionReturnProcessor;
	}

	/**
	 * @return the screenDisplayId
	 */
	public String getScreenDisplayId()
	{
		return screenDisplayId;
	}

	/**
	 * @param screenDisplayId
	 *           the screenDisplayId to set
	 */
	public void setScreenDisplayId(final String screenDisplayId)
	{
		this.screenDisplayId = screenDisplayId;
	}

	/**
	 * @return the isInvalidatingUI
	 */
	public boolean isInvalidatingUI()
	{
		return isInvalidatingUI;
	}

	/**
	 * @param isInvalidatingUI
	 *           the isInvalidatingUI to set
	 */
	public void setInvalidatingUI(final boolean isInvalidatingUI)
	{
		this.isInvalidatingUI = isInvalidatingUI;
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// CONNECTION / DISCONNECTION IN CASE OF STANDALONE APPLICATIONS
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * In case of a stand-alone application this method execute the {@link BaseConnectAction} with given parameters
	 * 
	 * @param connectionData
	 *           the {@link ConnectionData}
	 */
	public void doConnect(final ConnectionData connectionData)
	{
		doConnect(connectionData, null);
	}

	/**
	 * In case of a stand-alone application this method execute the {@link BaseConnectAction} with given parameters
	 * 
	 * @param connectionData
	 *           the {@link ConnectionData}
	 * @param customChainActionIfLoginSucceded
	 *           an eventual chain action to execute if connection succeeded (it may be null)
	 */
	public void doConnect(final ConnectionData connectionData, final BaseAction<?> customChainActionIfLoginSucceded)
	{
		BaseConnectAction.get(connectionData, customChainActionIfLoginSucceded).execute();
	}

	/**
	 * In case of a stand-alone application this method execute the {@link BaseDisconnectAction}
	 */
	public void doDisconnect()
	{
		BaseDisconnectAction.get().execute();
	}

	/**
	 * Call onBeforeDisconnect() on the current screen if not null.
	 * 
	 * @param currentAction
	 *           the current chain action. To be able to restart it after.
	 * @return whether or not the user can disconnect now. For example if there are things to be saved before you can
	 *         return false in this method and display a pop-up asking to save.
	 */
	public boolean onBeforeDisconnect(final BaseAction<?> currentAction)
	{
		return this.currentScreen == null || this.currentScreen.onBeforeDisconnect(currentAction);
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// EVENT HANDLING
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * @see Component#fireEvent(GwtEvent)
	 * @param event
	 *           the event
	 */
	public void fireEvent(final GwtEvent<?> event)
	{
		getGui().getViewport().fireEvent(event);
	}

	@Override
	public HandlerRegistration addDisconnectEventHandler(final DisconnectEventHandler handler)
	{
		return getGui().getViewport().addHandler(handler, DkDisconnectEvent.getType());
	}
}
